- Bass and Bytes/
- Articles/
- Busy, Not Nosy: a One-Way Sync from My Personal Calendar to Google Workspace/
Busy, Not Nosy: a One-Way Sync from My Personal Calendar to Google Workspace
Table of Contents
“Man is not truly one, but truly two.”
Robert Louis Stevenson, “Strange Case of Dr Jekyll and Mr Hyde”
As a consultant, my calendar is not only about me. At work we handle customer data that is better kept private, and calendars are part of that perimeter: a meeting title, an attendee list, or a description can tell a lot about who we’re working with and on what. My personal calendar, on the other hand, lives on Outlook.com (the good old Live account), and the two have always been separate worlds.
This works fine until real life starts touching working hours. Taking the car to the mechanic before work, a transfer to catch a plane, a doctor’s appointment at 18:00: these things are on my personal calendar, but my colleagues can’t see them, so they see a free slot and book a call right on top of it. The workaround was creating the same event twice, by hand, on both calendars. You can imagine how well that went.
There’s also another reason. I’m using an AI personal assistant agent that manages my personal calendar for me, so it is going to write events on my Live calendar. I want those events to block time at work automatically, without me copying anything, and I don’t want that agent to know about my work data and potentially mess with it (or, even worse, leak it).
This works for me, but maybe not for you #
Before describing the solution, I want to start with a disclaimer. This solution makes sense because of how my setup already looks: I keep work and personal stuff on separate devices and separate accounts. I wrote about why I bought a personal MacBook, and one of the main reasons was exactly this separation. My personal Mac only knows about my Live calendar, while my work account stays on work devices.
If you use the same laptop and the same browser profile for everything, your problem is probably different, and you might not need any of this. Also, please check your company’s policy before running scripts in your work account: what’s allowed there is your admin’s decision, not mine.
The options I considered #
The first idea was a third-party calendar sync service. There are several, and some of them are built exactly for the “show busy at work, show details to me” use case. The problem is that to do their job they need read access to the whole work calendar, which means customer names and meeting titles reach a third party, even if only “Busy” gets written on the other side. That’s the opposite of what I wanted.
The second idea was a two-way mirror: an anonymized copy of the work calendar going to Live, and personal events going to work. It works, but it means work data (even if reduced to busy blocks) leaving the work account, plus loop prevention so that copies don’t get copied back. More moving parts, more things that can break.
Then, while fiddling with Outlook.com, I noticed you can share your calendar with a contact and give them edit rights. I thought I could share it with my work account and be done. Unfortunately, that only works between Microsoft accounts: a Google account can’t accept the invitation, and the only thing it can use is the ICS link, which is read-only.
After some back and forth on the design with Claude, we came up with a solution: a one-way sync, from Live to work, and nothing in the other direction. My personal data flows into the work account, and nothing about work ever leaves my Google Workspace calendar. In fact, the agent doesn’t need to read that: it can assume my standard workday (9:30 to 18:30, leaving home at 9:00 and coming back at 19:00) and I’ll add the exceptions by hand.
Why not just subscribe to the ICS link? #
Google Calendar can subscribe to an ICS link (“Other calendars”, “From URL”), and that was my first attempt. There are two problems with it.
The first is that a subscribed calendar doesn’t count for free/busy: I can see my personal appointments, but my colleagues don’t, so they can still book time in that slot. The second issue is that Google refreshes subscribed feeds on its own schedule, and it can take several hours (some documentation says up to 12) for a change to show up. I added a test event and waited, and waited.
How it works #
I didn’t know about that, but your Google account has a feature called Google Apps Script that runs inside your account (Workspace or personal). With Apps Script, you can build a script that every 15 minutes downloads the ICS feed of a published calendar and creates a copy of each event on the primary work calendar.
To make events really private, the script uses fields available on calendar events (I didn’t know they existed, but AI knows). If you set an event as private, colleagues see only “busy”, and if you add the opaque property it actually blocks scheduling. To make things work without duplicating events on every sync, you can set a hidden extended property with the ID of the original event. Basically, this tag lets the script recognize its own events, so it only ever creates, updates, or deletes copies it made, and work events are never touched.
function buildCopy(key, src) {
const title = CONFIG.COPY_TITLE && src.summary ? src.summary : CONFIG.FALLBACK_TITLE;
const body = {
summary: CONFIG.TITLE_PREFIX + title,
start: src.start,
end: src.end,
visibility: 'private', // colleagues see "busy" only
transparency: 'opaque', // actually blocks scheduling
location: CONFIG.COPY_LOCATION ? src.location : '',
reminders: CONFIG.REMINDERS === 'default'
? { useDefault: true }
: { useDefault: false, overrides: CONFIG.REMINDERS },
extendedProperties: { private: { [TAG]: '1', [TAG_ID]: key } },
};
if (CONFIG.COLOR_ID) body.colorId = CONFIG.COLOR_ID;
return body;
}
Each run finds what should exist and compares against what already exists, creating missing copies and updating the ones that have been modified. If something has been removed, the copies are deleted. Please note that there’s no state to keep between runs: I didn’t want to have another data source just to maintain state.
Parsing the ICS is the part where I didn’t want to reinvent anything. Recurring events are the tricky bit: a weekly series with one occurrence skipped, another one moved by an hour, a third cancelled, and a daylight saving time change in the middle. The script uses ical.js, the parser that also powers Thunderbird, pasted as a second file in the Apps Script project. It expands the series, applies the exceptions, and resolves the time zones Outlook embeds in the feed.
A small detail, but to keep into consideration: if the download fails, or Microsoft returns something that isn’t a calendar, the script stops before touching anything. Without this check, an empty response would look like “all events deleted”, and the next run would wipe every copy.
function fetchIcs() {
const url = PropertiesService.getScriptProperties().getProperty('LIVE_ICS_URL');
if (!url) throw new Error('Set the LIVE_ICS_URL script property first.');
const res = UrlFetchApp.fetch(url, { muteHttpExceptions: true, followRedirects: true });
const text = res.getContentText();
if (res.getResponseCode() !== 200 || text.indexOf('BEGIN:VCALENDAR') === -1) {
throw new Error(`ICS fetch failed (HTTP ${res.getResponseCode()}), skipping this run.`);
}
return text;
}
Making it look nice (and not ringing twice) #
After implementing the script and having that satisfying shot of dopamine when I see everything work as planned, I found a different issue. I saw every personal event twice: once from the Live subscription and another as a copy. The solution was simple, I removed the subscription, and on the iPhone I deselected the Live calendar from the Calendar app. I still have it in the Outlook app, so now the Calendar app shows a single view, coming from my work account.
To recognize personal events at a glance, the copies get a 🏠 prefix in the title and a different color (Basil green, in my case). Both are options in the configuration, as well as whether to copy the title at all: since copies are private, colleagues only see “busy” anyway, but if you prefer to keep titles out of the work account you can turn it off and get a generic “Busy”.
Notifications needed a bit of thinking. With two calendars showing the same event, you get two notifications, so at first I disabled reminders on the copies. Then, after moving to a single view on the iPhone, I turned them back on (using the calendar’s default notifications) and disabled notifications in the Outlook app instead. The rule is simple: each event should notify you from one place only.
The nice surprise was “Time to Leave”. On my personal Mac I add locations to appointments and use the reminder that tells you when to leave. That option is computed by the Apple device and doesn’t travel through the ICS feed, but once the copies carry the location, the iPhone computes it on the work calendar too. My default notification is “Time to Leave”, and it just works.
Some things to keep in mind #
First, the script fixes Google’s delay, not Microsoft’s. Outlook.com may serve a cached version of the published feed, so a brand new event can still take a while to appear. For anything planned at least a few hours ahead, which is most of my life admin, this is fine.
Second, the ICS link is effectively a password to your personal calendar. The script reads it from Script Properties, so it doesn’t end up in the code if you share it or push it to GitHub (please don’t commit it). If it leaks, you can reset it from Outlook.com.
Third, the script needs permission to connect to an external service to download the feed. Your Workspace admin can see that scope, or restrict it. Data only flows into the work account, but it’s still something your admin should be fine with.
Last, by default the script skips all-day events (I don’t want a birthday to block my whole day) and events marked as “free” on Live. Both are configurable. Apps Script is free within its daily quotas, and a fetch every 15 minutes is far below them.
Setting it up #
The code is on GitHub: googleworkspaces-external-ics-sync. The setup takes about ten minutes:
- On Outlook.com, go to Settings, Calendar, Shared calendars, and publish your calendar. “Can view titles and locations” is enough. Copy the ICS link.
- Create a new project on script.google.com with your work account and paste
external-ics-sync.gs. - Add a second script file named
icaland pasteical.gsinto it. - In Services, add the Google Calendar API (keep the identifier
Calendar). - In Project Settings, add a Script Property called
LIVE_ICS_URLwith the ICS link. - Run
preview(): it logs what it finds in the feed without touching your calendar, and asks for authorization the first time. - Run
installTrigger()to sync every 15 minutes.
If you want to undo everything, removeAllCopies() deletes every event the script created, and then you can delete the trigger.
Conclusion #
This is not a big project, it’s less than 300 lines of JavaScript plus a parser, but it solved a problem that annoyed me every week. My colleagues see me as busy when I’m at the mechanic, nothing about my work leaves the work account, and my agent only needs to write on one calendar.
If you find this solution useful for you, let me know in the comments!